using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.IO;

namespace Bamboo.Utils
{
    /// <summary>
    /// 运行一个进程
    /// </summary>
    public class ProcessExecute
    {
        /// <summary>
        /// 要执行的进程文件，如果只有文件名，则会从Path环境变量中自动搜索
        /// </summary>
        public string FileName { get; set; }
        /// <summary>
        /// 带参数运行
        /// </summary>
        public string Arguments { get; set; }
        /// <summary>
        /// 程序运行的起始目录
        /// </summary>
        public string WorkingDirectory { get; set; }
        /// <summary>
        /// 是否忽略执行错误，默认不忽略（出错就抛异常）
        /// </summary>
        public bool IgnoreError { get; set; } = false;
        /// <summary>
        /// 是否开启调试模式，默认为false；开启调试模式时会额外输出调试信息
        /// </summary>
        public bool DebugMode { get; set; } = false;
        /// <summary>
        /// 等待进程的超时时间，默认3600秒
        /// </summary>
        public int TimeoutSeconds { get; set; } = 3600;
        /// <summary>
        /// 执行结束后1号管道的输出信息保存在这里
        /// </summary>
        public StringBuilder OutputText = new StringBuilder(128 * 1024);
        /// <summary>
        /// 执行结束后2号管道的错误信息保存在这里
        /// </summary>
        public StringBuilder ErrorText = new StringBuilder(128 * 1024);

        /// <summary>
        /// 保存下原先应该输入到错误流但是强制转为正确流的日志
        /// </summary>
        public StringBuilder ForceSuccessText = new StringBuilder(128 * 1024);

        /// <summary>
        /// 与OutputText相同，为了兼容旧版本留下的接口
        /// </summary>
        public StringBuilder OutputBuffer { get { return OutputText; } }
        /// <summary>
        /// 与ErrorText相同，为了兼容旧版本留下的接口
        /// </summary>
        public StringBuilder ErrorBuffer { get { return ErrorText; } }

        /// <summary>
        /// 如果不为空，则输出信息保存到指定的文件
        /// </summary>
        public string OutputFileName { get; set; }
        /// <summary>
        /// 如果不为空，则错误信息保存到指定的文件
        /// </summary>
        public string ErrorFileName { get; set; }
        /// <summary>
        /// 输出信息回调
        /// </summary>
        public Action<string> OutputHandler;
        /// <summary>
        /// 错误信息回调
        /// </summary>
        public Action<string> ErrorHandler;
        /// <summary>
        /// 要遮掩的输出内容，比如密码
        /// </summary>
        public HashSet<string> MaskWords { get; private set; } = new HashSet<string>();
        /// <summary>
        /// 强行成功标志，如果该集合不为空，那么在运行过程中会自动匹配输出，
        /// 匹配到了就直接杀死进程并强行置返回值为0。
        /// 用于某些后台运行的程序无法自动停止的场景
        /// </summary>
        public HashSet<string> ForceSuccessWords { get; private set; } = new HashSet<string>();
        /// <summary>
        /// 需要隐藏的输出内容。
        /// </summary>
        public HashSet<string> HideWords { get; private set; } = new HashSet<string>();

        /// <summary>
        ///  如果错误输出流中包含以下关键字,则转为正常输出流
        /// </summary>
        public HashSet<string> OutToNormalWords { get; private set; } = new HashSet<string>();

        /// <summary>
        /// 执行批处理时关闭命令回显，默认为true。（该参数仅对RunBat有效）
        /// </summary>
        public bool EchoOff { get; set; } = true;

        /// <summary>
        /// 输入输出的字符集，默认为GBK
        /// </summary>
        public string EncodingName { get; set; } = "gbk";
        /// <summary>
        /// 可选的输入文本, 在运行开始后, 有时候需要手工输入一些字符, 否则命令行会卡死.
        /// </summary>
        public string InputText { get; set; }
        /// <summary>
        /// 上次执行的批处理文件内容
        /// </summary>
        public string BatContent { get; private set; } = "";
        /// <summary>
        /// 进程返回值，只有进程运行结束后才有意义
        /// </summary>
        public int ExitCode { get; private set; } = int.MinValue;
        /// <summary>
        /// 是否发现强行成功标志
        /// </summary>
        private bool _foundForceSuccessWord = false;

        /// <summary>
        /// 当遇到某个关键词时，输入一段文本
        /// </summary>
        public List<ProcessExecuteExpect> ExpectInputs { get; } = new List<ProcessExecuteExpect>();
        /// <summary>
        /// 构造函数
        /// </summary>
        public ProcessExecute()
        {

        }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="filename"></param>
        /// <param name="arguments"></param>
        public ProcessExecute(string filename, string arguments) : this()
        {
            this.FileName = filename;
            this.Arguments = arguments;
        }

        private ProcessStartInfo GetStartInfo()
        {
            var si = new ProcessStartInfo();
            si.FileName = SearchExe(FileName);
            si.Arguments = this.Arguments;
            si.WorkingDirectory = this.WorkingDirectory;
            si.WindowStyle = ProcessWindowStyle.Hidden;
            si.CreateNoWindow = true;
            si.UseShellExecute = false;
            si.RedirectStandardOutput = true;
            si.RedirectStandardError = true;
            si.RedirectStandardInput = true;
            si.StandardOutputEncoding = Encoding.GetEncoding(EncodingName);
            si.StandardErrorEncoding = si.StandardOutputEncoding;

            return si;
        }

        public static string GetTempFile(string prefix, string postfix)
        {
            FileInfo file = null;
            Random rand = new Random();
            string prefix2 = prefix + "_p" + Process.GetCurrentProcess().Id +
                "t" + System.Threading.Thread.CurrentThread.ManagedThreadId + "r";
            for (int i = 0; i < 9999; ++i)
            {
                file = new FileInfo(prefix2 + rand.Next(1000, 9999) + postfix);
                if (!file.Exists) break;
            }
            if (file.Exists) throw new Exception("获取临时文件名失败，文件已存在：" + file);
            return file.FullName;
        }

        private void Validate()
        {
            if (TimeoutSeconds < 0 || TimeoutSeconds > 360000) throw new Exception("超时时间必须介于0~360000秒之间，当前值：" + TimeoutSeconds);
        }

        private string SearchExe(string exefile)
        {
            if (File.Exists(exefile)) return exefile;

            if (exefile.EndsWith(".exe") && !exefile.Contains("\\"))
            {
                var pathenv = Environment.GetEnvironmentVariable("PATH");
                var paths = pathenv.Split(';');
                foreach (var path in paths)
                {
                    string exe2 = "";
                    try
                    {
                        exe2 = Path.Combine(path.Trim('\\', ' ', '"'), exefile);
                        if (File.Exists(exe2))
                        {
                            LogDebug("从Path环境变量中找到待执行程序：" + exe2);
                            return exe2;
                        }
                    }
                    catch (Exception)
                    {
                        //路径中具有非法字符，直接跳过
                        LogDebug("警告：Path环境变量拼接后的路径有问题，跳过该路径：" + exe2);
                    }
                }
            }

            throw new Exception("从Path环境变量中搜索不到执行文件: " + exefile);
        }

        /// <summary>
        /// 将待执行的批处理命令保存到当前目录的.bat临时文件中，然后执行它，执行完后删除临时文件
        /// </summary>
        /// <param name="command"></param>
        public void RunBat(string command)
        {
            this.BatContent = command;
            string batFileName = GetTempFile("_processExecute", ".bat");
            LogDebug("创建临时批处理文件：" + batFileName);
            if (EchoOff) command = "@echo off\r\n" + command;
            try
            {
                File.WriteAllText(batFileName, command, Encoding.GetEncoding("gbk"));
                FileName = batFileName;
                Arguments = "";
                Run();
            }
            finally
            {
                if (File.Exists(batFileName)) File.Delete(batFileName);
            }
        }


        private string GetDebugInfo()
        {
            string info = "路径：" + FileName + ", 参数：" + Arguments +
                    "，起始目录：" + WorkingDirectory + "，超时：" + TimeoutSeconds + "秒，忽略错误：" + IgnoreError;
            return info;
        }

        /// <summary>
        /// 执行程序
        /// </summary>
        /// <returns>进程的ExitCode</returns>
        public int Run()
        {
            var sw = Stopwatch.StartNew();
            OutputText.Clear();
            ErrorText.Clear();
            _foundForceSuccessWord = false;
            ExitCode = -998; //代表退出代码没有被正确赋值。
            LogDebug("外部程序执行开始，" + GetDebugInfo());

            var si = GetStartInfo();

            Validate();
            bool isBatFile = si.FileName.EndsWith(".bat");
            string batFileContent = "";
            if (isBatFile)
            {
                batFileContent = File.ReadAllText(si.FileName, Encoding.GetEncoding("gbk"));
                LogDebug("批处理文件的内容：" + batFileContent);
            }
            using (var p = new Process())
            {
                p.StartInfo = si;
                p.OutputDataReceived += new DataReceivedEventHandler((sender, e) =>
                {
                    InnerLog(p, e.Data, false);
                });
                p.ErrorDataReceived += new DataReceivedEventHandler((sender, e) =>
                {
                    InnerLog(p, e.Data, true);
                });
                p.Start();
                p.BeginOutputReadLine();
                p.BeginErrorReadLine();
                if (!string.IsNullOrEmpty(InputText))
                {
                    lock (p.StandardInput)
                    {
                        p.StandardInput.WriteLine(InputText);
                        p.StandardInput.Flush();
                    }
                }

                //等待停止
                var expireTime = DateTime.Now.AddSeconds(TimeoutSeconds);
                while (!p.HasExited)
                {
                    //判断成功标志
                    if (_foundForceSuccessWord)
                    {
                        p.StandardInput.WriteLine("\r\n");
                        LogDebug("输出中找到了成功标志，强行杀死进程并置返回值为0。");
                        try { p.Kill(); } catch (Exception ex) { LogDebug("强行杀死进程失败：" + ex.Message); }
                        ExitCode = 0;
                        break;
                    }
                    //判断超时
                    var tt = DateTime.Now;
                    if (tt > expireTime)
                    {
                        try { p.Kill(); } catch (Exception ex) { LogDebug("强行杀死进程失败：" + ex.Message); }
                        ExitCode = -999;
                        throw new Exception("等待进程结束超时");
                    }
                    System.Threading.Thread.Sleep(32);
                }
                if (!_foundForceSuccessWord)
                {
                    p.WaitForExit();
                    ExitCode = p.ExitCode;
                }
                else
                {
                    //有可能进程先结束了，输出还在跑，到这里才找到成功标志。
                    ExitCode = 0;
                }
                p.Close();
            }
            SaveOutput();
            sw.Stop();
            CheckExitCode(sw.ElapsedMilliseconds);
            return ExitCode;
        }

        private string GetBatContent()
        {
            bool isBatFile = FileName.EndsWith(".bat");
            if (isBatFile)
            {
                string s = File.ReadAllText(FileName, Encoding.GetEncoding("gbk"));
                return s;
            }
            else
            {
                return "";
            }
        }

        private void CheckExitCode(long elapsedMilliseconds)
        {
            if (ExitCode != 0)
            {
                string batContent = GetBatContent();
                string errorMsgBase = "耗时：" + elapsedMilliseconds + "ms，返回值：" + ExitCode;
                if (!string.IsNullOrWhiteSpace(batContent)) errorMsgBase += "\r\n批处理内容：" + batContent;
                else errorMsgBase += "\r\n文件名：" + FileName + "\r\n参数：" + Arguments;
                string shortErrorText = ErrorText.ToString();
                if (shortErrorText.Length > 4096) shortErrorText = shortErrorText.Substring(0, 4096);
                errorMsgBase += "\r\n错误输出：" + shortErrorText;
                errorMsgBase = FilterMaskWords(errorMsgBase);
                if (IgnoreError)
                {
                    LogDebug("外部程序执行报错，但继续运行，" + errorMsgBase);
                }
                else
                {
                    throw new Exception("外部程序执行报错，" + errorMsgBase);
                }
            }
            else
            {
                LogDebug("外部程序执行成功，耗时：" + elapsedMilliseconds + "ms");
            }
        }

        private void SaveOutput()
        {
            var enc = Encoding.GetEncoding(this.EncodingName);
            lock (OutputText)
            {
                if (!string.IsNullOrEmpty(OutputFileName) && OutputText.Length > 0)
                {
                    File.WriteAllText(OutputFileName, OutputText.ToString(), enc);
                }
                if (!string.IsNullOrEmpty(ErrorFileName) && ErrorText.Length > 0)
                {
                    File.WriteAllText(ErrorFileName, ErrorText.ToString(), enc);
                }
            }
        }

        private string FilterMaskWords(string input)
        {
            //处理隐藏
            foreach (var word in HideWords)
            {
                if (!string.IsNullOrWhiteSpace(word) && input.Contains(word)) return "";
            }
            //处理遮掩
            foreach (var word in MaskWords)
            {
                if (!string.IsNullOrWhiteSpace(word) && input.Contains(word)) input = input.Replace(word, "***");
            }
            return input;
        }

        /// <summary>
        /// 处理错误流，将错误流转到正常流输出
        /// </summary>
        /// <param name="input">入参</param>
        /// <param name="logInDebug">是否将结果转到正常溜输出</param>
        /// <returns></returns>
        private string FilterErrorWords(string input, out bool logInDebug)
        {
            logInDebug = false;
            //需要转为正常的输出
            foreach (var word in OutToNormalWords)
            {
                if (!string.IsNullOrWhiteSpace(word) && input.Contains(word))
                {
                    logInDebug = true;
                }
            }
            return input;

        }

        private void FindForceSuccessWord(string input)
        {
            if (_foundForceSuccessWord) return;
            foreach (var key in ForceSuccessWords)
            {
                if (input.Contains(key))
                {
                    _foundForceSuccessWord = true;
                    return;
                }
            }
        }
        private void LogDebug(string s)
        {
            if (!DebugMode) return;
            InnerLog(null, s, false);
        }

        private bool InnerLog(Process p, string s, bool isError)
        {
            lock (OutputText)
            {
                if (string.IsNullOrWhiteSpace(s)) return false;
                s = s.Trim();
                //处理期望测试
                if (this.ExpectInputs.Count > 0 && p != null)
                {
                    foreach (var item in ExpectInputs)
                    {
                        DealExpect(p, item, s);
                    }
                }
                if (ForceSuccessWords.Count > 0 && !_foundForceSuccessWord)
                {
                    FindForceSuccessWord(s);
                }
                s = FilterMaskWords(s);
                bool logInComm = false;
                s = FilterErrorWords(s, out logInComm);
                if (string.IsNullOrWhiteSpace(s)) return false;
                //记录到结果
                OutputText.AppendLine(s);
                if (isError && !logInComm)
                {
                    ErrorText.AppendLine(s);
                }
                else if (isError && logInComm)
                {
                    ForceSuccessText.AppendLine(s);
                }
                //回调
                if (isError && !logInComm && ErrorHandler != null)
                {
                    ErrorHandler(s);
                }
                else if (OutputHandler != null)
                {
                    OutputHandler(s);
                }
            }
            return true;
        }

        private void DealExpect(Process proc, ProcessExecuteExpect e, string output)
        {
            if (proc == null) return;
            if (e.Valid == false) return;
            if (output.Contains(e.ExpectText))
            {
                lock (proc.StandardInput)
                {
                    proc.StandardInput.WriteLine(e.InputText);
                }
                if (e.OnlyOnce) e.Valid = false;
            }
            if (!string.IsNullOrWhiteSpace(e.InvalidExpect) && output.Contains(e.InvalidExpect))
            {
                e.Valid = false;
            }
        }
    }

    public class ProcessExecuteExpect
    {
        public string ExpectText;
        public string InputText;
        /// <summary>
        /// 当本字段不为空时，代表遇到该文本后本对象置无效
        /// </summary>
        public string InvalidExpect;
        /// <summary>
        /// 只触发一次，默认为true
        /// </summary>
        public bool OnlyOnce = true;
        /// <summary>
        /// 是否有效，默认为true
        /// </summary>
        public bool Valid = true;
    }
}
